package simulator;

import graph.DijkstraTime;
import graph.Graph;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;

/**
 * Trida reprezentujici rozhodovaci strategii firmy. Tato trida je urcena pro planovani a
 * preplanovavani tras.
 * 
 * @author Petr Altman
 *
 */
public class DecisionStrategy {

	private int actualTime;
	
	private DecisionStrategyListener listener = null;
	
	private Graph graph;
	
	private ArrayList<Truck> trucks;
	
	private DijkstraTime<City> dijkstraTime = null;
	
	/**
	 * Cstor.
	 * @param listener Listener udalosti rozhodovaci strategie.
	 */
	public DecisionStrategy(DecisionStrategyListener listener) {
		super();
		this.listener = listener;
	}

	/**
	 * Spusti rozhodovaci strategii a vrati stav objednavky - je vyrizena, neni vyrizena.
	 * @param trucks Seznam dostupnych nakladnich vozu.
	 * @param order Objednavka ke zpracovani.
	 * @param actualTime Referencni aktualni cas.
	 * @return True - objednavka byla prijata. False - objednavka nebyla prijata.
	 */
	public boolean execute(ArrayList<Truck> trucks, Order order, int actualTime) {
		boolean result = true;
		
		this.trucks = trucks;
		this.actualTime = actualTime;
		this.graph = order.getCompany().getWorld();

		PathResult bestResult = null, newResult = null;
		
		dijkstraTime = new DijkstraTime<City>(this.graph);
		dijkstraTime.execute(order.getOwner(), null, true);
		
		// naplanovani trasy s prihlednutim na jiz existujici nakladni vozy
		bestResult = getBestPathResultForOrder(order);
		// naplanovani nove trasy
		newResult = getNewTrackPathResult(order);
		
		if(newResult == null && bestResult == null) {
			// odmitnuti objednavky
			if(listener != null)
				listener.onDecisionRefuseOrder(order);
			result = false;
		} else
		if(newResult == null && bestResult != null) {
			// pouziti nejvhodnejsiho nakladaku
			if(listener != null)
				listener.onDecisionUseTruck(bestResult.getOrder(), bestResult.getTruck(), bestResult.getPaths());
		}
		else if(newResult != null && bestResult == null) {
			// vytvoreni noveho nakladaku
			if(listener != null)
				listener.onDecisionNewTruck(order, newResult.getPaths().get(0));
		} else if(newResult != null && bestResult != null) {
			if(newResult.getDistance() < bestResult.getDistance()) {
				// vytvoreni noveho nakladaku
				if(listener != null)
					listener.onDecisionNewTruck(order, newResult.getPaths().get(0));
			} else {
				// pouziti nejvhodnejsiho nakladaku
				if(listener != null)
					listener.onDecisionUseTruck(bestResult.getOrder(), bestResult.getTruck(), bestResult.getPaths());
			}
		}

		return result;
	}
	
	/**
	 * Naplanuje pro objednavku uplne novou trasu.
	 * @param order Objednavka ke zpracovani.
	 * @return Naplanovana trasa.
	 */
	private PathResult getNewTrackPathResult(Order order) {
		PathResult result = new PathResult(null, order);
		
		TruckPath path = new TruckPath(order);
		
		Path p = new Path(this.dijkstraTime.getShortestPathNodes(order.getCompany().getCity()));
		Collections.reverse(p);
		path.setPath(p, actualTime);
		
		// overeni zda je mozne objednavku stihnout vyridit
		if(path.isTimeAcceptable(actualTime)) {
			result.addPath(path);
		} else {
			return null;
		}
		
		return result;
	}
	
	/**
	 * Najde nejvhodnejsi nakladni vuz pro vyrizeni objednavky a vrati nove naplanovanou trasu tohoto vozidla.
	 * @param order Objednavka ke zpracovani.
	 * @return Naplanovana trasa pro objednavku.
	 */
	private PathResult getBestPathResultForOrder(Order order) {
		
		PathResult bestResult = null;
		
		for (Truck truck : trucks) {
			// overeni zda je nakladak schopen splnit objednavku
			if(
					truck.hasEnoughGoods(order.getGoodsCount()) && 
					truck.cenBeReplanned()) {
				PathResult result = getPossibleTruckPathForOrder(truck, order);
				if(result != null) {
					if(bestResult == null || bestResult.getDistance() > result.getDistance()) {
						bestResult = result;
					}
				}
			}
		}

		return bestResult;
	}

	/**
	 * Najde nejvhodnejsi trasu nakladniho vozu pro splneni dane objednavky.
	 * @param truck Nakladni vuz.
	 * @param order Objednavka ke zpracovani.
	 * @return Naplanovana trasa nakladniho vozu pro objednavku.
	 */
	private PathResult getPossibleTruckPathForOrder(Truck truck, Order order) {
		PathResult result = new PathResult(truck, order);
		
		HashMap<Order, Path> truckPaths = new HashMap<Order, Path>();
		
		// vytvoreni seznamu setridenych objednavek dle zacatku vykladaciho okna
		// a ulozeni aktualnich cest do pomocne hashmapy
		SortedSet<Order> sortedOrders = new TreeSet<Order>(orderComparator); 		
		for(TruckPath path : truck.getPaths()) {
			// ulozeni objednavky do setrideneho setu
			sortedOrders.add(path.getOrder());
			// ulozeni cesty do hashmapy pod danou objednavkou
			truckPaths.put(path.getOrder(), path.getPath());
		}
		// pridani do setrideneho seznamu objednavek aktualni objednavku
		sortedOrders.add(order);
						
		// nalezeni cest mezi setridenymi objednavkami
		City startCity = truck.getActualPath() == null ? order.getCompany().getCity() : truck.getActualPath().getOrder().getOwner();
		int startTime = truck.getActualPath() == null ? actualTime : truck.getActualPath().getEndTime();
		int i = 0;
		boolean replanPath = false, first = true;
		for(Order o : sortedOrders) {
			
			Path planedPath = null;
			
			// pokud se jedna o danou objednavku, naplanuj ji cestu
			if(o == order) {
				// test, zda objednavka nebyla zarazena k vyrizeni pred jiz vyrizenymi objednavkami.
				if(truck.getActualPath() != null && i < truck.getActualPathIndex()) {
					result = null;
					break;
				}
				
				// cesta z mesta dane objednavky do mesta, ze ktereho se nakladak vyda
				planedPath = new Path(dijkstraTime.getShortestPathNodes(startCity));
				// prevraceni poradi
				Collections.reverse(planedPath);
				
				replanPath = true;
			} else if(replanPath) {
				// cesta z mesta dane objednavky do mesta aktualni objednavky
				planedPath = new Path(dijkstraTime.getShortestPathNodes(o.getOwner()));
				
				replanPath = false;
			} else {
				// ziskani jiz naplanovane cesty pro objednavku
				planedPath = truckPaths.get(o);
			}
			
			// vytvoreni trasy
			TruckPath path = new TruckPath(o);
			path.setPath(planedPath, startTime);
			
			// test zda cesta byla nalezena
			if(path.getPath() == null) {
				result = null;
				break;
			}
			
			// pokud se jedna o neprojete cesty na trasa, tak over moznost casoveho splneni objednavek
			if(truck.getActualPath() == null || i > truck.getActualPathIndex()) {
				// overeni zda lze startovaci cas akceptovat vzhledem k vykladacimu okenku
				if(!path.isTimeAcceptable(startTime)) {
					result = null;
					break;
				} else if(!first){
					path.setStartTime(startTime, startTime);
				}
			}
			
			// pridani planovane cesty do vysledku
			result.addPath(path);
			
			// nastaveni referencniho startovaciho casu pro nasledujici cestu
			startTime = path.getEndTime();
			
			startCity = o.getOwner();
			
			first = false;
			i++;
		}
		
		return result;
	}
	
	/**
	 * Komparator objednavek rozhodovaci strategie.
	 */
	private static final Comparator<Order> orderComparator = new Comparator<Order>() {
		
		@Override
		public int compare(Order o1, Order o2) {
			return o1.getOwner().getWorktimeStart() - o2.getOwner().getWorktimeStart();
		}
	};
	
	/**
	 * Trida reprezentujici naplanovanou trasu rozhodovaci strategii.
	 * @author Petr Altman
	 *
	 */
	private class PathResult {
		private Truck truck;
		
		/**
		 * Vrati trasu.
		 * @return trasa.
		 */
		public Vector<TruckPath> getPaths() {
			return paths;
		}

		private Vector<TruckPath> paths;

		/**
		 * Vrati prirazeny nakladni vuz.
		 * @return Nakladni vuz.
		 */
		public Truck getTruck() {
			return truck;
		}
		
		private Order order;
				
		/**
		 * Vrati prirazenou objednavku.
		 * @return Objednavka.
		 */
		public Order getOrder() {
			return order;
		}

		private int distance = 0;
		
		/**
		 * Vrati vzdalenost k mestu, ktere zadalo prirazenou objednavku.
		 * @return
		 */
		public int getDistance() {
			return distance;
		}

		
		/**
		 * Prida cestu do vysledne trasy.
		 * @param path Cesta.
		 */
		public void addPath(TruckPath path) {
			paths.add(path);
			
			if(path.getOrder() == order)
				distance = path.getLength();
		}

		/**
		 * Cstor.
		 * @param truck Nakladni vuz prirazeny vysledne naplanovane trase.
		 * @param order Objednavka prirazena vysledne naplanovane trase.
		 */
		public PathResult(Truck truck, Order order) {
			this.truck = truck;
			this.order = order;
			this.paths = new Vector<TruckPath>();
		}
	}
}
